package hotciv.common;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map.Entry;

import hotciv.framework.*;

/** Skeleton implementation of HotCiv.

   This source code is from the book 
     "Flexible, Reliable Software:
       Using Patterns and Agile Development"
     published 2010 by CRC Press.
   Author: 
     Henrik B Christensen 
     Computer Science Department
     Aarhus University

   This source code is provided WITHOUT ANY WARRANTY either 
   expressed or implied. You may study, use, modify, and 
   distribute it for non-commercial purposes. For any 
   commercial use, see http://www.baerbak.com/
 */

public class GameImpl implements Game {
	private AgingStrategy ager;
	private int worldAge;
	private WinnerStrategy worldWinner;
	private ActionStrategy actionStrategy;
	private BattleStrategy battleStrategy;
	private CityPopulationStrategy populationStrategy;
	private CityResourceStrategy resourceStrategy;
	private int playerInTurn;
	private Player[] players;
	private int[] playerWins;
	private int playerWinCounter;
	private int numberOfRounds = 0;
	private HashMap<Integer, Integer> movesMade;
	private HashMap<City, Integer> productionMap;
	private Player winner;
	private WorldMap world;
	private ArrayList<GameObserver> gameObservers;


	public GameImpl(HotCivFactory factory) 
	{
		movesMade = new HashMap<Integer, Integer>();
		productionMap = new HashMap<City, Integer>();
		gameObservers = new ArrayList<GameObserver>();
		winner = null;
		playerInTurn = 0;
		worldAge = -4000;
		this.ager = factory.agingStrategy();
		this.worldWinner = factory.winnerStrategy();
		this.actionStrategy = factory.actionStrategy();
		this.battleStrategy = factory.battleStrategy();
		this.populationStrategy = factory.cityPopulationStrategy();
		this.resourceStrategy = factory.cityResourceStrategy();
		players = new Player[2];
		players[0] = Player.RED;
		players[1] = Player.BLUE;
		playerWins = new int[players.length];
		playerWinCounter = 0;

		this.world = new WorldMap(factory.tileMap(), factory.unitMap(), factory.cityMap());

	}
	
	
	@Override
	public Tile getTileAt( Position p ) 
	{ 
		return world.getTileAt(p); 
	}

	@Override
	public Unit getUnitAt( Position p ) 
	{ 
		return world.getUnitAt(p);
	}

	@Override
	public City getCityAt( Position p ) 
	{
		return world.getCityAt(p);
	}

	@Override
	public Player getPlayerInTurn() 
	{ 
		return players[playerInTurn]; 
	}
	
	@Override
	public Player getWinner() 
	{
		if (winner != null)
			return winner;
		else
			return null;
	}

	@Override
	public int getAge() 
	{
		return worldAge;
	}

	@Override
	public boolean moveUnit( Position from, Position to ) 
	{
		Unit unitFrom = getUnitAt(from);
		Unit unitTo = getUnitAt(to);

		// Is it the players own unit?
		if (unitFrom.getOwner() != getPlayerInTurn()) return false;
		// Is the to position empty or an opponent?
		if (unitTo != null && unitTo.getOwner() == getPlayerInTurn()) return false;

		if (!isWalkableTileAt(to)) return false;
		int d = getWorldDistance(from, to);
		if (!canUnitMoveThisFar(d, unitFrom, from)) return false;

		movesMade.put(getUniqueUnitKey(unitFrom, to), d);
		
		// Moves to empty tile or overrides opponent on position to
		world.removeUnitAt(from);
		worldChangedAt(from);
		
		if (unitTo == null || fightBattle(to, unitFrom)) world.setUnitAt(to, unitFrom);
		else return true; 	// Doesn't look for city if it can't move
		City city = getCityAt(to);
		if(city != null && city.getOwner() != unitFrom.getOwner()) // there's a city and it's not yours!
		{
			world.setCityAt(to, new CityImpl(unitFrom.getOwner(), city.getProduction(), city.getWorkforceFocus())); // now it is! 
		} 
		worldChangedAt(to);

		return true;
	}

	@Override
	public void endOfTurn() 
	{

		movesMade.clear();
		int player = playerInTurn+1;
		//End of round
		if (player == players.length) {
			worldAge = ager.getNewAge(worldAge);
			//Set production for cities
			for(City c : world.getAllCities()) {
				resourceStrategy.calculateCityResources(c, world);
				
				CityImpl city = (CityImpl) c;
				if (city.getProductionStock() >= getUnitCost(c.getProduction())) {
					//New unit
					newUnitAtCity(c);
					city.setProductionStock( city.getProductionStock() - getUnitCost(c.getProduction()) );
				}
				
				populationStrategy.calculatePopulation(city);
				
			}
			numberOfRounds++;
		}
		if (worldWinner.getWinner(this) != null) {
			winner = worldWinner.getWinner(this);
			return;
		}  
		playerInTurn = player%players.length;
		for (GameObserver o : gameObservers) o.turnEnds(getPlayerInTurn(), getAge());
	}
	
	@Override
	public void changeWorkForceFocusInCityAt( Position p, String balance )
	{
		CityImpl city = (CityImpl) getCityAt(p);
		city.setWorkforceFocus(balance);
		//world.setCityAt(p, new CityImpl(city.getOwner(), city.getProduction(), balance));
	}
	@Override
	public void changeProductionInCityAt( Position p, String unitType ) 
	{
		CityImpl city = (CityImpl) getCityAt(p);
		city.setProduction(unitType);
		//world.setCityAt(p, new CityImpl(city.getOwner(), unitType, city.getWorkforceFocus()));
	}
	@Override
	public void performUnitActionAt( Position p ) 
	{  
		actionStrategy.performAction(p, world);
		worldChangedAt(p);
	}

	public int getRoundCount() {
		return numberOfRounds;
	}
	
	
	@Override
	public void addObserver( GameObserver observer )
	{
		gameObservers.add(observer);
	}
	
	@Override
	public void setTileFocus(Position p) {
		for (GameObserver o : gameObservers) o.tileFocusChangedAt(p);
	}
	
	public int movesLeftForUnit(Unit u, Position p) {
		if (!movesMade.containsKey(getUniqueUnitKey(u, p))) return u.getMoveCount();
		else return u.getMoveCount() - movesMade.get(getUniqueUnitKey(u, p));
	}

	/** private helper methods **/
	private void worldChangedAt(Position p) {
		for (GameObserver o : gameObservers) o.worldChangedAt(p);
	}
	
	private int getUniqueUnitKey(Unit u, Position p) {
		return u.hashCode()+p.hashCode();
	}
	
	private int getUnitCost(String unitType) {
		if (unitType.equals(GameConstants.ARCHER)) return 10;
		else if (unitType.equals(GameConstants.LEGION)) return 15;
		else if (unitType.equals(GameConstants.SETTLER)) return 30;
		else return 0;
	}

	private boolean isWalkableTileAt(Position p) {
		if (p.getRow() < 0 || p.getRow() >= GameConstants.WORLDSIZE || p.getColumn() < 0 || p.getColumn() >= GameConstants.WORLDSIZE 
				|| getTileAt(p).getTypeString() == GameConstants.OCEANS || getTileAt(p).getTypeString()==GameConstants.MOUNTAINS) return false;
		else return true;
	}

	private int getWorldDistance(Position from, Position to) {
		int dX = Math.abs(from.getRow()-to.getRow());
		int dY = Math.abs(from.getColumn()-to.getColumn());
		return Math.max(dX, dY);
	}

	private boolean canUnitMoveThisFar(int distance, Unit unit, Position atPosition) {
		if (distance > unit.getMoveCount() || distance > movesLeftForUnit(unit, atPosition)) return false;
		else return true;
	}

	private boolean newUnitAtCity(City c) {
		Position p = world.getCityPosition(c);
		if (p != null) {
			Unit u = new UnitImpl(c.getProduction(), c.getOwner());
			placeUnitAroundCity(p, u);
			return true;
		} else return false;
	}

	private void placeUnitAroundCity(Position p, Unit u) {
		int[][] positions =  { {0,0}, {0, -1}, {1, -1}, {1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1} };
		for (int i=0;i<9;i++) {
			p = new Position(p.getRow()+positions[i][0], p.getColumn()+positions[i][1]);
			if (isWalkableTileAt(p) && getUnitAt(p)==null) {
				world.setUnitAt(p,u);
				worldChangedAt(p);
				break;
			}
		}
	}
	
	private boolean fightBattle(Position battlefield, Unit attacker) 
	{
		if(battleStrategy.determineBattle(this, battlefield, attacker))
		{
			playerWins[attacker.getOwner().ordinal()]++;
			return true;
		}
		else
			return false;		
	}
	
	public void resetPlayerWins()
	{
		for(int i = 0; i < playerWins.length; i++)
			playerWins[i] = 0;
	}
	
	public int getPlayerWins(Player player)
	{
		return playerWins[player.ordinal()];
	}



	
}
